视频基础知识
本期作者
姜军
B端技术中心资深开发工程师
01 常见的颜色表示方法
人眼能看到东西是因为光线进入了眼睛,打在视网膜上,而视网膜上的神经和大脑相连。
1.1 黑白
当我们提到一张图片是“黑白”的时候,或者说“黑白电视”的时候,实际上并不代表它只有黑色和白色两种颜色,一个很容易理解的事情是,从白变成黑,如果这个过程是连续的,那么过程中会出现我们管它叫“灰色”的颜色。我们很容易定义“黑”,没有光线进入眼睛的时候,那就是黑;但是相对的,“白”就会更复杂一些。举个简单的例子,比如我们手上有一个能发出白光的灯泡,当对灯泡通电的时候,它就会发出白色的光。但是大家都知道,“灯泡”这种东西,根据耗电量的不同,有亮一些的和暗一些的各种不同的规格。如果当我们看到第一个灯泡发出的光,说它是白色;那当第二个更亮的灯泡亮起来的时候,我们还会说第二个灯泡发出的光是更亮的白色。于是在“白色”这个颜色上面,多了个“亮”这样的属性。在显示器上,我们通常会将它能够显示的最大亮度叫做“白色”,能显示的最低亮度叫做“黑色”。
从黑色到白色的变化过程可以简单看作是一条线段,一端是“黑”、另一端是当前设备能发出的最大亮度的“白”。那么可以把黑的那一个点记为0,白的那一个点记为1,大于0而小于1的部分就成为了“灰”。可以用一个数字来表示只有黑白的颜色。
1.2 RGB
光线会刺激人眼内的锥状细胞,这些细胞通过视神经和大脑相连。不同的细胞对不同波长的光有不同的敏感度。这些对不同波长的光敏感的细胞主要分为三种,一种对波长在560nm左右的光敏感、一种对530nm左右的光敏感而另一种对420nm左右的光敏感。这些波长分别对应红色、绿色、蓝色三种颜色的光。平时说的人类的眼睛只能识别这三种颜色的光就来源于此。自然界的光是由许多不同波长的光组合而成,但只要能还原其中红绿蓝(也就是Red、Green、Blue,简称RGB)的部分,对人眼看起来是没有区别的。所以在针对人类的应用中,可以只记录红绿蓝三种分量的数据。
基于这种理论,就可以把上面那个“黑白”的记法进行扩展,从一条线段变成三条线段,分别记录红色光的强度、绿色光的强度和蓝色光的强度。如此,用一个数字可以用来表示黑白的颜色,而用三个数字就可以表示出彩色了。注意,虽然这里说是三条线段,但并不代表三条线段的顶端所代表的的光的强度是相同的,因为在定义中,当红、绿、蓝“等量”(比如,都是1.0)混合的时候,产出的颜色为白色。这里的等量并不是指三种频率的光的强度相等,而是强度的比例为某种固定的比例。
尽管如此,”红色、绿色、蓝色“这样的描述仍然不够准确。事实上,看同一个频道的节目,不同的电视机显示出来的颜色虽然大致一样但又略有偏差的事情或许大家都遇到过:有的电视鲜艳一些,有的土一些,还有的偏红、偏蓝。为了规范统一,国际电信联盟无线电通信部门(ITU-R)利用国际照明协会的XYZ色彩表示方式来定义了R、G、B分别是什么颜色,以及等量混合的时候的白色是什么颜色。在不同的标准里,R、G、B事实上可以表示不同的颜色,开发中常遇到的标准可以搜索白皮书里的具体参数定义。例如 BT.601、BT.709 中对 RGB 的定义和 sRGB 标准相同。
1.3 YUV
人眼对于亮度和颜色的感受是由不同的细胞完成,对亮度的感受比较敏感而对色度的感觉比较迟钝,所以在需要节省存储空间的时候,可以为亮度保存更准确的数据,为色度保存更不准确的数据。YUV的数据表示形式很适合这种场景。
YUV保存的彩色数据,可以看作给黑白图像上色。其中Y通道保存的就是这张黑白图像,而U和V就是给黑白图像上色。因为人眼对亮度敏感,所以Y部分的数据可以保存得精度比较高,而U和V就可以保存得相对模糊。
在RGB中,不论是R还是G还是B,其中一个通道亮度比较高,这个颜色看起来就会比较亮。所以计算亮度的时候,R和G和B都有参与。RGB和YUV之间互相转换的公式如下
Y = Kr * R + Kg * G + Kb * B
U = (B - Y) / (1 - Kb)
V = (R - Y) / (1 - Kr)
其中,R、G、B的范围是 0 ~ 1,Y的范围是 0 ~ 1,UV的范围是 -1 ~ 1。Kr Kb Kg 是三个常系数,根据选用标准的不同而不同,可以在对应标准的白皮书中找到,例如 BT.601、BT.709、BT.2020。用错标准的时候,从YUV转成RGB并显示会导致偏色。为了检查偏色,通常会使用 ARIBColorBars、PM5544 等测试卡来进行检查。其中PM5544可能大家会比较熟悉。
根据公式可知,这是一个三元一次方程组,把方程2、3中的Y使用方程1代入,就可以得到从RGB计算YUV的方法;给出YUV解方程组,可以得到RGB。它们之间就是这么互相转换的。
在实际开发过程中,YUV的取值范围有两种不同的标准。例如用8bit来表示YUV数据的时候,一种标准是使用完整的0~255范围,而另一种是使用Y的16~235、UV的16~240范围。前者被叫做Full Range,或者PC Range。后者被叫做Partial Range,或者TV Range。后者反而比较常见。
1.4 光电转换
光电转换是通过光伏效应把光能量转换为电能的过程。例如在数码相机中,光线通过镜头,打在感光元件上,感光元件产生电能,输送给芯片进行进一步的处理和保存。当要显示图像的时候,芯片会将存储的数据加载进来,然后将电信号通过显示屏等元件转换为光信号给人观看。
在液晶屏幕普及之前,人们普遍使用的电视机和电脑显示器都是俗称“大屁股”的阴极射线管屏幕。这种设备的原理是通过电子轰击屏幕上的荧光涂层,使之发出不同亮度不同颜色的光来显示画面,这个叫做电光转换。但在这个过程中人们发现,将电压提升一倍之后,画面的亮度并没有简单地提升一倍,它们之间存在一种非线性的函数关系。通常屏幕的亮度和输入电压的关系是,亮度约为输入电压的2.2到2.4次方,称之为电光转换函数。
因为存在这种转换函数关系,光信号通过摄像机转换为电信号的时候也要做一次转换来抵消电光转换函数,称之为光电转换函数。光电转换函数接近于电光转换函数的逆函数。
对于光电转换和电光转换,搜索关键字 OETF 和 EOTF 可以找到相关的资料。标准动态范围(SDR)视频来说,对应白皮书标准BT.1886;对于高动态范围(HDR)的视频来说,光电转换是需要特别关注的一个过程,与SDR差别较大,对应白皮书:BT.2100。
选用适当的光电和电光转换函数可以提升数据保存的效率,因为人眼对光强度的敏感也不是线性的,当光线比较暗的时候,很小的强度不同人眼可以感觉到;但是当光很强的时候,同样小强度的变化,人眼就感觉不到了。所以可以设计这样一种光电转换曲线,它在光弱的时候,电信号的变动大一些;当光比较强的时候,同样程度的变化,体现在电信号的变化上更小。
1.5 采样精度
也叫做“深度”(depth)。保存所谓“光的强度”的时候,有个“保存精度”的问题。我们都知道,计算机内数据的保存和处理,都是二进制数字。那么用几个二进制数据保存这些数据,就是“精度”。平时会说的 8bit 10bit 就是属于这个范畴。8bit 指的是用8个二进制数来表示采集到的光的强度,那么它就能够保存 2的8次方 一共 256 种不同的情况;如果使用的是 10bit 来保存,那么这里是 1024。假设这里保存的是黑白形式的数据,那么在 8bit 下,0代表黑色,255代表白色;如果在 10bit 下,那么0代表黑色,1023代表白色。如果保存的是彩色形式的数据,那么就是三个数字。
02 画面
将许许多多不同颜色的点铺满一个面,就形成了一张画面。
2.1 分辨率
说到分辨率很多人会认为这就是画面有多大。但是其实这不太准确,因为一张画面有多大实际上是取决于你显示器有多大,或者显示图片的窗口开了多大。就像家里买电视看电视节目,以前小时候家里电视是14寸的那么电视节目显示出来就是14寸、买21寸的那么电视节目显示出来就是21寸。电视节目并不会因为你买的是14寸的电视就显示不完整,也不会因为你买的是21寸的电视它就只在电视屏幕上的一小块地方显示。
那么有没有什么办法可以更直观地对分辨率有概念?假想一下,现在有个电视屏幕,上面显示的内容是黑白相间的条纹,就像斑马那样。这个条纹越来越密越来越密,到最后变成一条条的细线,再细下去就看不清了。那么我有这样一个画面,画面上的内容就是黑白相间的条纹,这个条纹越来越密越来越密,到最后黑色和白色的条纹糊在了一起,这种情况下就再也没有办法数清楚到底有多少根黑色线、多少根白色线了。这个极限状态就是它的分辨率。
在电脑上,因为有像素的概念,所以可以直接用像素来表示分辨率。黑白相间的条纹最多能显示多少个,就取决于有多少个像素。因为在多媒体领域,像素的形状并不总是正方形的,所以同样分辨率的一张画面,可以是不同比例的。
2.2 宽高比
宽高比指的是画面的宽度和高度的比例。虽然大多数情况下,这与横向的分辨率和纵向的分辨率的比例是相同的,但并不总是相同的。一个例子,在DVD时代,碟片上保存的画面都是720x480分辨率的,但是画面的高宽比却有4:3和16:9两种。这就是像素形状不是方形的情况。通常描述高宽比的时候,会用到“采样比例(Sample Aspect Ratio)”、“像素比例(Pixel Aspect Ratio)”、“显示比例(Display Aspect Ratio)”这样的概念,这些比例都是宽和高的比。所以 横向分辨率÷纵向分辨率×采样比例=显示比例。
03 画面的存储方式举例
3.1 RGB
最常见的RGB数据是每个采样(或者说像素)使用三个8bit整数分别表示红、 绿、蓝通道的信号强度,这样每一个像素就是3个字节。整张画面上的像素,按照从左到右的顺序保存一行的所有像素,保存完一行保存下一行。因为每个像素是24bit,所以也会写成RGB24的。同理,也有保存的顺序是反过来的BGR24,区别只在于先存放蓝色通道的数据。根据需要,有的时候会多一个辅助通道Alpha,根据Alpha通道存放的位置不同,于是也有 ARGB32、RGBA32、BGRA32、ABGR32 之类之类。
例如,一个4x4的画面,保存下来的数据大概长这样:
RGB RGB RGB RGB
RGB RGB RGB RGB
RGB RGB RGB RGB
RGB RGB RGB RGB
实际开发中经常会遇到的另一个点是,每一行数据的后面还会带一些其他数据,比如用来让每一行数据的字节数都是某个整数的整倍之类。在保存这种数据的数据结构里,除了高和宽,就还会有一个叫 stride 的属性,用来表示每一行到底几个字节。在读写指定位置的像素数据的时候,如果不按照这个 stride 来定位所在的行,就会出现读写的数据偏了的情况。
3.2 YUV420P
有的地方写的是I420,是同一种东西。作为另一种典型的格式,YUV的保存方式常见的与RGB是不同的。常见的YUV数据保存方式,是先存所有像素的Y通道数据,然后存U通道,再存V通道。然后Y和U和V还不一定就是存在连续的内存空间里,而是可能为三个不同的内存块。不在内存中表示,或者需要存盘、通过管道传输之类的时候,就是Y后面跟着U、U后面跟着V这样的存储方式。
上述4x4的画面,保存下来的数据就是:
YYYY
YYYY
YYYY
YYYY
UUUU
UUUU
UUUU
UUUU
VVVV
VVVV
VVVV
VVVV
但是前面说到,人眼对亮度敏感,对色度更没那么敏感,所以YUV420P8在保存数据的时候,U和V的分辨率会小于Y的分辨率:高和宽都是一半。例如这个4x4的画面,如果YUV三个通道拆开来看,那么只有Y通道是完整的4x4数据,U和V都是2x2数据。所以实际上保存的数据是:
YYYY
YYYY
YYYY
YYYY
UU
UU
VV
VV
而YV12则是把其中的U和V位置互换了一下。
通过这种方式缩减,就从一个像素占用24bit(3个字节)变成了一个像素平均占用12bit(1.5个字节),数据量一下子减少了一半。但是因为人眼对色度不敏感,所以其实不太能看出来。
此外也有YUV420P10,YUV420P12,YUV420P16等保存方式,把原本8bit保存的数据扩展到了10bit、12bit或者16bit,使其具有更高的精度。但是不管是10bit、12bit还是16bit,保存的时候都是占用2个字节。10bit会用低10位,前面补0;12bit会有低12位,前面补0;16bit占用完整的2个字节。
和RGB的数据一样,YUV数据在实际开发中也会遇到 stride 的问题,也要按照同样的方法操作,否则也是一样读偏了或者写偏了。
3.3 NV12
NV12和NV21是更换了数据保存方式的YUV420P8。它保存的Y通道的分辨率也是全量的,保存的UV通道的分辨率是横向和纵向分别为Y通道的一半。但是它只有两个通道、而不像YUV420P有三个通道。U和V数据是在第二个通道里交错存放的。还是以上面的4x4画面为例,保存的数据是:
YYYY
YYYY
YYYY
YYYY
UVUV
UVUV
如果是NV21,那么是:
YYYY
YYYY
YYYY
YYYY
VUVU
VUVU
04 视频
一系列连续的图片按照一定的速度进行顺序播放,人眼看起来就像是画面中的东西在动,这就形成了视频。
4.1 帧率
简单的理解帧就是为视频或者动画中的每一张画面,而视频和动画特效就是由无数张画面组合而成,每一张画面都是一帧。帧率的单位是FPS(frame per second),即每秒多少帧。
4.2 视频编码(压缩)
视频编码最重要的目的也是为了进行数据压缩,以此来降低数据传输和存储成本。除了前面说的用YUV表示、并把UV的分辨率降低之外,还有很多其他的手段。
基于画面存储方式的例子,可以得知尽管使用了YUV420P方式存储画面,但平均一个像素也会占用12bit。那么一张1280x720分辨率的画面,就会占用1280x720x12=11059200bit;而在一秒钟有30帧的场景下,一秒钟的画面就占了11059200x30=331776000bit,换算过来约40MB。这是一个很大的数,平时看的24分钟一集的动画片,就要55.6GB。这对于存储和传输都会造成很大的压力。所以需要采用编码压缩技术来减少数据量。
视频压缩是一个专业性非常强、非常复杂的领域,门槛也很高。通常做开发的时候,是调用现有的编码器,调节它的参数,不会涉及到编码器内部的改动。所以对其原理只要有简单的了解就能应付大多数场景了。
视频压缩有别于平时说的文件压缩。文件压缩大多会采用类似7z、ZIP等格式,对应LZMA、Inflate算法。这些算法都是“无损压缩”算法,翻译过来就是解压后得到的数据,和压缩前的数据是完全一致的。视频压缩虽然也有无损压缩,但更多时候使用的是有损压缩,顾名思义解压后得到的数据和压缩前的数据,从数据层面来说不完全一样,但是画面的内容,从人眼来看,是差不多的。压缩的时候,又要损又要人眼不太看得出来,那么这用的是什么手段……
先从简单一些的图片压缩开始。大家都用过JPG格式,也见过JPG压缩压过头了,图片上出现方块、颜色溢出,甚至还有经典的“你表情包都绿了”。这里有损压缩靠的是离散余弦变换,有兴趣的同学可以自己去搜索和学习离散余弦变换是什么东西,这里只最简单带过一下。假设我现在有一张 8x8 的黑白图片,那么这个图片就一共64个像素。我们把这64个像素的亮度(灰度?)记作y1 y2 y3 ... y64。
那我现在再来64个方程,如下:
k1_1*x1 + k2_1*x2 + k3_1*x3 + ... + k64_1*x64 = y1
k1_2*x1 + k2_2*x2 + k3_2*x3 + ... + k64_2*x64 = y2
k1_3*x1 + k2_3*x2 + k3_3*x3 + ... + k64_3*x64 = y3
....
k1_64*x1 + k2_64*x2 + k3_64*x3 + ... + k64_64*x64 = y64
如果k的值全部已知,得到64个未知数64个方程,可以把里面的x1 x2 x3 ... x64解出来。如果现在有两个人A和B,他们事先都定好k的值,随后A把算出来的这些x告诉B,B套入上面的公式,就能算出y。
因为上面的式子实在太多了,为了简化,就用向量来表示,所以上面那一串就可以等价地写成:
(k1_1, k1_2, k1_3, ... k1_64)*x1 + (k2_1, k2_2, k2_3, ... k2_64)*x2 + ... + (k64_1, k64_2, k64_3, ... k64_64)*x64 = (y1, y2, y3, ... y64)
太长了不方便看,所以写成下面形式:
k1*x1 + k2+x2 + k3*x3 + ... + k64*x64 = y
从64个y的值变成64个x的值,有什么好处呢?为了说明这个问题,我们先来定义这些 k 的值。
下图是一个有64个小格子的图,每个格子一共有8x8一共64个像素。因为这些k都是64维的向量,所以用k1代表第一个格子里的内容,按照右图顺序类推,一直到k64代表最后一个格子的内容。然后k1_1代表第一个格子里的第一个像素,k1_2代表第一个格子里的第二个像素,以此类推。亮暗代表不同的值,全白为1、全黑为0。
假设y的64个值,用8x8的图形表示是这样的:
套入上述方程组可以求出x1 x2 ... x64。然后这64个x的值,传给了另一个人。这个人收到这64个x的值之后,要套入上面的方程把y的值算出来。如果把算的过程表示出来的话:
k1*x1
k1*x1 + k2*x2
k1*x1 + k2*x2 + k3*x3
...
k1*x1 + k2*x2 + k3*x3 + ... +k64*x64
动画中,最左边的块是每一步的计算结果,中间的块代表 k 乘以 x 的值,最右边的块代表k的值。可以看到,随着计算一步一步进行,计算结果在视觉上会越来越接近原画面。那么如果我不传64个值,只传63个,虽然画质损失了,但是收到的人能看出来是字母A,那不就少了传输一个数值的需要了吗?不过实际操作中不是这样,实际操作中,越后面的系数对应的未知数,会保存精度越低的值;对压缩的需求越大保存的精度越低,这就是有损压缩的基本原理。
在有损压缩完了之后,还会过一遍无损压缩。JPG使用的无损压缩方式是最小生成树。这个算法对于科班的计算机专业的学生来说,是课上老师会教的东西。我尝试用一个很简单的说法来说,就是本来我一个文件,比如是1000个字节,每个字节8个比特,就是8000比特。那如果我现在发现我这文件里全写的都是大写字母A和大写字母B,没别的东西,那我直接用0来代表A、1来代表B,每个字节变成了1个bit,文件就从8000bit变成了1000bit,变成原来的1/8了。这个算法的原理是在文件中,越经常出现的文字用越少比特数来表示、越不经常出现的文字用越长的比特数来表示,原本固定的一个字节8个bit就变成了1个字节不一定多少个bit,两边都约定好到底每个字节怎么表示,需要保存的内容就变少了。并不是所有编码都用的最小生成树,其他格式的编码也会有其他的无损压缩算法。
JPG里用的技术相对较少,现在用的视频编码里还要再多很多技术。比如其中有一个技术就是,如果一个画面里有很多长得一样的东西,它会记录类似“把哪个块复制到哪里哪里”这类的信息,于是这些块里面的数据就只要记录一次就够了。这种情况叫做“空间上的冗余“。
视频由许许多多这样的图片组成,因为除了场景切换之类的情况外,视频这一帧的内容对比上一帧有一部分是没有变化的,又有一部分是画面上的物体在移动。那么如果下一张画面里,通过记录“哪里的东西,现在运动到了哪里”来保存和前一帧对比后有不同的数据,那么数据量可以大大减少。
比如《魔法少女小圆》的片头动画,在这个场景中,中间的人物是不动的,但是左下角的云在飘。将编码器记录的运动数据可视化之后,可以看到这些数据确实是“左下角的云在朝着左下角运动”。
视频编码里还有许许多多不同的技术,这里就只介绍几个最基本的,对这个有点概念,需要深钻的话还请自己搜索需要的资料进一步学习。